#==============================================================================
#
#  $Id$
#
"""
    New style interface.
    
    You should not import this module directly - its name is likely to change. 
    Get it from "import odb" instead.
"""
#
#   Copyright (C) 2006 Michael A. Muller
#   Portions Copyright 2008 Google Inc.
#
#   This file is part of ODB.
#
#   ODB is free software: you can redistribute it and/or modify it under the 
#   terms of the GNU Lesser General Public License as published by the Free 
#   Software Foundation, either version 3 of the License, or (at your option) 
#   any later version.
#
#   ODB is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU Lesser General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public License 
#   along with ODB.  If not, see <http://www.gnu.org/licenses/>.
#
#==============================================================================

import functools, re, threading
import full, mem
from mem import MemKeyValCursor

class _ThreadTxn(threading.local):
    
    def __init__(self):
        self.txn = None
    
class OverwriteError(Exception):
    """
        Raised when a put() would overwrite an existing value and 
        errorOnOverwrite is set.
    """
    pass

class IntegrityError(Exception):
    """
        Raised when a secondary index references an element that doesn't exist 
        in the primary table.
    """
    pass

class NoGlobalStoreError(Exception):
    """
        Raised by setStore() if the global datastore has not been defined.
    """
    pass

class Table:
    
    # the "not found" object is specified as a default during a get.  It 
    # allows us to diferentiate between a None value and a key that is not in 
    # the map (allowing us to raise a KeyError in this case)
    class __NotFound:
        pass
    _notFound = __NotFound()
    
    def __init__(self, store, table, name):
        """
            Constructor.  You should normally call Store.getMap() instead.
            
            parms:
                store: [Store] the datastore object.
                table: [full.FullKeyDB or full.FullListDB] the underlying 
                    database object.
                name: [str] the table name
        """
        self._store = store
        self._impl = table
        self._name = name

    def delete(self, keyOrIndex):
        """
            Delete the specified object from the database.
            
            parms:
                keyOrIndex: [str or int] This is a string if the underlying 
                    database is a Map, an integer if it is a Sequence.  The 
                    key or index to delete.
        """
        txn = self._store._threadInfo.txn
        self._impl.delete(txn, keyOrIndex)

class Map(Table):
    """
        A map table.  Map tables map string keys to value objects.
    """

    def get(self, key, errorIfMissing=False):
        txn = self._store._threadInfo.txn
        if errorIfMissing:
            
            # if the user wants a KeyError if the key is not defined, pass in 
            # the notFound object as a default and check for it.
            result = self._impl.get(key, self._notFound, txn = txn)
            if result == self._notFound:
                raise KeyError(key)
            return result
        else:
            return self._impl.get(key, txn = txn)

    def put(self, key, val, errorOnOverwrite=False):
        txn = self._store._threadInfo.txn
        self._impl.set(txn, key, val)
    
    def cursor(self):
        """
            Returns a cursor that iterates over key value pairs of the map.
        """
        txn = self._store._threadInfo.txn
        return self._impl.cursor(txn)

    def getMapIndex(self, name):
        """
            Returns a map-style index for the specified table (MapIndex).
        """
        assert self._store._nameRx.match(name), \
               'Index names must be python-style identifiers'
        fullName = '%s.%s' % (self._name, name)
        return self._store._getTable(fullName, MapIndex, 'map index',
                                      self._store._getBTreeDB,
                                      self
                                      )

class MapIndexCursor(MemKeyValCursor):
    """Cursor specialization for MapIndeces."""
    
    def __init__(self, baseTable, indexTable, implCursor, txn):
        MemKeyValCursor.__init__(self, indexTable, implCursor, txn)
        self.__baseTable = baseTable
    
    def next(self):
        k, v = MemKeyValCursor.next(self)
        try:
            return k, self.__baseTable.get(v, errorIfMissing=True)
        except KeyError:
            raise IntegrityError('Database integrity check failed: '
                                  '%s references %s which is undefined.' %
                                  (repr(k), repr(v))
                                 )

    def prev(self):
        k, v = MemKeyValCursor.prev(self)
        try:
            return k, self.__baseTable.get(v, errorIfMissing=True)
        except KeyError:
            raise IntegrityError('Database integrity check failed: '
                                  '%s references %s which is undefined.' %
                                  (repr(k), repr(v))
                                 )

class MapIndex(Map):

    def __init__(self, store, table, name, baseTable):
        Map.__init__(self, store, table, name)
        self.__baseTable = baseTable

    def get(self, key, errorIfMissing=False):
        # get the primary key, then look up the value
        
        try:
            # we always want a KeyError, we'll re-raise or return None as 
            # appropriate.
            primaryKey = Map.get(self, key, errorIfMissing=True)
        except KeyError:
            if errorIfMissing:
                raise
            else:
                return None

        try:
            return self.__baseTable.get(primaryKey, errorIfMissing=True)
        except KeyError:
            raise IntegrityError('Database integrity check failed: '
                                  '%s references %s which is undefined.' %
                                  (repr(key), repr(primaryKey))
                                 )

    def link(self, key, primaryKey):
        """
            Link the index key to the primary key.
            
            parms:
                key: [str] index key
                primaryKey: [str] main table primary key.
        """
        self.put(key, primaryKey)
    
    def cursor(self):
        txn = self._store._threadInfo.txn
        # XXX minor breaches of encapsulation.
        implCursor = self._impl._impl.cursor()
        if not txn: txn = self._impl._env._getTmpTxn()
        return MapIndexCursor(self.__baseTable, self._impl, implCursor, txn)

class Sequence(Table):
    """
        A sequence table.  Sequence tables are like lists: you can access and 
        insert at specific indeces.
    """
    
    def get(self, index, errorIfMissing=False):
        """
            Gets the object at the specified index.
            
            parms:
                index: [int]
                errorIfMissing: [bool] If true, raise an IndexError if the
                    index is out of range, otherwise return None.
        """
        txn = self._store._threadInfo.txn
        if errorIfMissing:
            val = self._impl.get(index, self._notFound, txn = txn)
            if val is self._notFound:
                raise IndexError('sequence table index out of range')
        else:
            return self._impl.get(index, txn = txn)

    def append(self, val):
        """
            Append the object to the sequence.
            
            parms:
                val: [any]
        """
        txn = self._store._threadInfo.txn
        return self._impl.append(val, txn = txn)
    
    def insert(self, index, val):
        """
            Insert a new value at the designated position in the sequence.

            Raises IndexError if the index is out of range.
            
            parms:
                index: [int]
                val: [any]
        """
        txn = self._store._threadInfo.txn
        return self._impl.insert(index, val, txn = txn)
    
    def pop(self, index = -1):
        """
            Remove the value at the specified index and return it.
           
            Raises IndexError if the index is out of range.
 
            parms:
                index: [int] I think the normal index semantics work, negative 
                    values are indexed from the end.
        """
        txn = self._store._threadInfo.txn
        return self._impl.pop(index, txn = txn)
    
    def __len__(self):
        txn = self._store._threadInfo.txn
        return self._impl.len(txn)

    def cursor(self):
        """
            Returns a cursor that iterates over the values in the sequence.
        """
        txn = self._store._threadInfo.txn
        return self._impl.cursor(txn)

class Store:
    """
        A datastore.
    
        This is the new-style interface.
    """
    
    _nameRx = re.compile(r'[a-z_]\w*$', re.I)
    
    def __init__(self, path, filer = None, checkpointCallback = None):
        """
            Creates a new datastore from the path.
            
            parms:
                path: [str] path to the datastore directory.  Will be created 
                    if it doesn't exist.
                filer: [mem.Filer or None] if specified, this is the filer to 
                    be used for opening and listing logfiles.
                checkpointCallback: [callable<Filer>] Function to be called 
                    after checkpointing (intended to be used for backup).
        """
        import full
        self._impl = full.FullEnv(path, filer = filer, 
                                  checkpointCallback = checkpointCallback
                                  )
        self.__cache = {}
        self._threadInfo = _ThreadTxn()
    
    def _getTable(self, name, type, typeName, implFactory, *extraArgs):
        if name in self.__cache:
            table = self.__cache[name]
            if not isinstance(table, type):
                raise TypeError('%s is not a %s table.' % (name, typeName))
        else:            
            try:
                
                # use a filename of "_" unless there is an existing table 
                # without the leading underscore.
                filename = '_' + name
                if not self._impl.hasTable(filename) and \
                   self._impl.hasTable(name):
                    filename = name
                    
                table = self.__cache[name] = \
                    type(self, 
                         implFactory(filename, txn = self._threadInfo.txn),
                         name,
                         *extraArgs
                         )
            except TypeError:
                raise TypeError('%s is not a %s table' % (name, typeName))
        
        return table
    
    def getMap(self, name):
        """
            Returns a new @Map table.  The table is created if it doesn't 
            already exist.

            parms:
                name: [str] the table name.
        """
        assert self._nameRx.match(name), \
               'Table names must be python-style identifiers'
        return self._getTable(name, Map, 'map', self._impl.getBTreeDB)
    
    def _getBTreeDB(self, name, txn):
        """
            Exposes the implementations getBTreeDB() method so we can create 
            indeces.
            
            parms:
                name: [str] table name
                txn: [@odb.odbi.Txn] the transaction object.
        """
        return self._impl.getBTreeDB(name, txn = txn)
    
    def getSequence(self, name):
        """
            Returns a new @Sequence table.  The table is created if it doesn't 
            already exist.
            
            parms:
                name: [str] the table name.
        """
        assert self._nameRx.match(name), \
               'Table names must be python-style identifiers'
        return self._getTable(name, Sequence, 'sequence', 
                              self._impl.getBListDB
                              )

    def __clearTxn(self, txn):
        "Callback function to clear the current transaction."
        assert self._threadInfo.txn
        self._threadInfo.txn = None
    
    def __popSubTxn(self, txn):
        "Callback function to pop a subtransaction."
        txn = self._threadInfo.txn
        assert txn and isinstance(txn, mem.SubTxn)
        self._threadInfo.txn = txn.parent

    def startTxn(self):
        """
            Start a new transaction and returns the Txn object.
            
            Nested transactions are not yet supported.
        """
        if self._threadInfo.txn:
            # already got a transaction in this thread: create a 
            # sub-transaction.
            self._threadInfo.txn = txn = \
                mem.SubTxn(self._impl, self._threadInfo.txn)
            txn.addCloseCallback(self.__popSubTxn)
        else:
            self._threadInfo.txn = txn = self._impl.getTxn()
            txn.addCloseCallback(self.__clearTxn)
        return txn

    def getTxn(self):
        """
            Returns the current transaction or None if there is none.
        """
        return self._threadInfo.txn

    def query(self, query):
        """
            Performs a string query on the datastore.  Produces an iterator 
            over the result set which will be key value tuples - (str, any).
            
            parms:
                query: (str) a query string.
        """
        raise NotImplementedError('XXX write me')
    
    def apply(self, txn):
        """
            Applies a transaction that was previously defer()red.
            
            parms:
                txn: [mem.MemTxn] the transaction to be applied.
        """
        self._impl.apply(txn)

_store = None

def setStore(store):
    """
        Set the global datastore instance.  This can also be passed a None to 
        effectively clear the global datastore instance.
        
        parms:
            store: [@Store or None]
    """
    global _store
    _store = store

def getStore():
    """
        Get the global datastore instance.  You must have called @setStore() 
        first.  Returns @Store.
        
        Raises NoGlobalStoreError if the global datastore has not been set.
    """
    if not _store:
        raise NoGlobalStoreError()
    return _store

def txnFunc(func):
    """
        Decorator that wraps a function in a transaction.
        
        parms:
            func: [callable]
    """
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        txn = getStore().startTxn()
        try:
            result = func(*args, **kwargs)
            
            txn.commit()
            txn = None
            return result
        finally:
            if txn: txn.abort()

    return wrapper
